39. SpringMVC4 - 异常处理, 运行流程, SpringMVC 与Spring 整合, SpringMVC Spring 关系

异常处理

DefaultHandlerExceptionResolver

Spring MVC 通过 HandlerExceptionResolver 处理程序的异常,包括 Handler 映射、数据绑定以及目标方法执行时发生的异常。SpringMVC 提供的 HandlerExceptionResolver 的实现类。DispatcherServlet 默认装配的 HandlerExceptionResolver。

DefaultHandlerExceptionResolver 对一些特殊的异常进行处理,比如:NoSuchRequestHandlingMethodException、HttpRequestMethodNotSupportedException、
HttpMediaTypeNotSupportedException、HttpMediaTypeNotAcceptableException等。源码如下:

例如下面发生的 405 方法未匹配异常

SimpleMappingExceptionResolver

可以用 SimpleMappingExceptionResolver 自己定义上述异常中没有的异常的处理,利用空指针异常等。在 SpringMVC 的配置文件中配置 SimpleMappingExceptionResolver

1
2
3
4
5
6
7
8
 <!-- 自定义异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings"> <!-- 定义一个异常映射 -->
<props>
<prop key="java.lang.NullPointerException">error</prop> <!-- 当方法空指针异常时候,跳转到 error 页面 -->
</props>
</property>
</bean>

定义Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.auguigu.test;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;


@Controller
public class TestController {

@RequestMapping(value = "testException", method = RequestMethod.POST)
@ResponseBody
public String testException() {
return "success";
}

@RequestMapping(value = "testException2")
@ResponseBody
public String testException2() {
// 构建一个空指针异常
String string = null;
string.substring(1);
return "seccess";
}
}

发生空指针异常后就能看到 error.jsp 页面。

代码地址

SpringMVC 运行流程

  1. 用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获;

  2. DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI): 判断请求URI对应的映射

    2.1不存在:

    ​ 2.1.1 再判断是否配置了mvc:default-servlet-handler:

    ​ 2.1.2 如果没配置,则控制台报映射查找不到,客户端展示404错误

    ​ 2.1.3 如果有配置,则执行目标资源(一般为静态资源,如:JS,CSS,HTML)

    2.2 存在:

    ​ 2.2.1 执行下面流程

  3. 根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain对象的形式返回;

  4. DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。

  5. 如果成功获得HandlerAdapter后,此时将开始执行拦截器的preHandler(…)方法【正向】

  6. 提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

    6.1 HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息

    6.2 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等

    6.3 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等

    6.4 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中

  7. Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象;

  8. 此时将开始执行拦截器的postHandle(…)方法【逆向】

  9. 根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver(必须是已经注册到Spring容器中的ViewResolver)返回给DispatcherServlet,根据Model和View,来渲染视图

  10. 在返回给客户端时需要执行拦截器的AfterCompletion方法【逆向】

  11. 将渲染结果返回给客户端

自建监听器进行 Spring 和 SpringMVC 的整合

通常情况下, 类似于数据源, 事务, 整合其他框架都是放在 Spring 的配置文件中(而不是放在 SpringMVC 的配置文件中)。放入 Spring 配置文件对应的 IOC 容器中的还有 Service 和 Dao。所以 Spring 的加载要在 SpringMVC 之前。即 DispatcherServlet 之前。所以我们可以在监听器里面创建 Spring 对象。

配置监听器

配置一个监听器,让其在 contextInitialized 的时候加载 Spring 的配置文件,创建 Spring IOC 容器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.atguigu.listener;

import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class SpringListener implements ServletContextListener{

@Override
public void contextDestroyed(ServletContextEvent sce) {
// TODO Auto-generated method stub

}

@Override
public void contextInitialized(ServletContextEvent sce) {
// 创建 Spring IOC 容器对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 可以将创建的 applicationContext 对象放入 ServletContext 中,以便在后续中
// 可以随时获取 Spring 管理的对象
ServletContext servletContext = sce.getServletContext();
servletContext.setAttribute("applicationContext", applicationContext);
}
}

在 web.xml 中应用此监听器让其生效。

1
2
3
4
<!-- 创建 listener。listener 中创建 Spring IOC 容器 -->
<listener>
<listener-class>com.atguigu.listener.SpringListener</listener-class>
</listener>

完成 SpringMVC 的配置

在 web.xml 中完成 SpringMVC 的剩余配置。例如设置 CharacterEncodingFilter,CharacterEncodingFilter,还有配置 DispatcherServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>SpringMVC04</display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>

<!-- 手动创建 listener。listener 中创建 Spring IOC 容器 -->
<listener>
<listener-class>com.atguigu.listener.SpringListener</listener-class>
</listener>

<!-- 设置编码解析器 -->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- 设置 HiddenHttpMethodFilter -->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<!-- DispatcherServlet -->
<servlet>
<servlet-name>springDispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:springMVC.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springDispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>

</web-app>

SpringMVC 还没有配置完成,在指定的 SpringMVC 的配置文件中设置组件的扫描,处理静态资源,处理 json,是否有自定义异常处理等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 扫描组件 -->
<context:component-scan base-package="com.atguigu.controller"></context:component-scan>

<!-- 视图解析器 -->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/view/"></property>
<property name="suffix" value=".jsp"></property>
</bean>

<!-- 处理静态资源 -->
<mvc:default-servlet-handler/>
<!-- 开启 MVC 驱动 -->
<!-- 开启 SpringMVC 自动将 Java 对象转换为 Json 的能力。(使用的是 jackson,需要导入 jar 包) -->
<mvc:annotation-driven/>

<!-- 自定义异常 -->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings"> <!-- 定义一个异常映射 -->
<props>
<!-- 当方法空指针异常时候,跳转到 error 页面 -->
<prop key="java.lang.NullPointerException">error</prop>
</props>
</property>
</bean>
</beans>

完成 Spring 的配置

还需要对 Spring 的配置文件进行配置,配置扫描组件的位置,配置数据源, 整合其他框架, 事务等。但是这里要注意,SpringMVC 管理了 Controller 层,也就是说 SpringMVC 的配置文件中会去扫描 Controller 层中 @Controller组件,那么 Spring 中就不要再去扫描了。可以使用 context:exclude-filter 来排除不需要扫描的包。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd">

<!-- 扫描组件。不扫描 Controller 层,因为 Controller 层由 SpringMVC 管理,SpringMVC 会去扫描 -->
<context:component-scan base-package="com.atguigu">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
</beans>

验证

我们在 Controller 中看是否能获取 Spring 管理的对象。如果能就说明二者的整合没问题(Controller 是 SpringMVC 管理,其余对象是 Spring 管理,在 Controller 中能获取 Spring 管理的对象,那就说明没问题。)

在创建监听器的时候,将 SpringIOC 容器塞入 ServletContext 中

1
2
3
4
5
6
7
8
9
@Override
public void contextInitialized(ServletContextEvent sce) {
// 创建 Spring IOC 容器对象
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("spring.xml");
// 可以将创建的 applicationContext 对象放入 ServletContext 中,以便在后续中
// 可以随时获取 Spring 管理的对象
ServletContext servletContext = sce.getServletContext();
servletContext.setAttribute("applicationContext", applicationContext);
}

在 Controller 中进行获取 Spring 管理的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Controller
public class TestController {

@ResponseBody
@RequestMapping("/testListaner")
public User testListaner(HttpSession session) {
// 从 HttpSession 中获取 ServletContext 对象
ServletContext servletContext = session.getServletContext();
// 从 ServletContext 中获取 Spring IOC 对象(我们在监听器中将 Spring IOC 对象存放在了 ServletContext 中)
ApplicationContext applicationContext = (ApplicationContext) servletContext.getAttribute("applicationContext");
// 从 Spring IOC 中获取 Spring 管理的 user 对象
User user = applicationContext.getBean("user", User.class);
return user;
}
}

使用自带监听器进行 Spring 和 SpringMVC 的整合

上面我们是自己创建了一个监听器,然后在监听器中创建 Spring IOC 容器,但是 Spring 官方给我们提供了一个监听器,可以直接使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 <!-- 手动创建 listener。listener 中创建 Spring IOC 容器 -->
<!--
<listener>
<listener-class>com.atguigu.listener.SpringListener</listener-class>
</listener>
-->

<!-- 使用 Spring自带的监听器创建 Spring IOC -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 指定 Spring 配置文件的位置。默认会去找 WEB-INF 下的 applicationContext.xml -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>

image-20200303225442885

第一个箭头可以直接创建 ContextLoaderListener,第二个箭头可以直接创建 DIspatcherServlet

访问测试,Controller 代码如下,测试 http://localhost:8080/ss/testListaner 是否能访问成功。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.atguigu.controller;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpSession;

import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import com.atguigu.bean.User;

@Controller
public class TestController {

@ResponseBody
@RequestMapping("/testListaner")
public String testListaner(HttpSession session) {
/**
// 从 HttpSession 中获取 ServletContext 对象
ServletContext servletContext = session.getServletContext();
// 从 ServletContext 中获取 Spring IOC 对象(我们在监听器中将 Spring IOC 对象存放在了 ServletContext )
ApplicationContext applicationContext = (ApplicationContext) servletContext.getAttribute("applicationContext");
// 从 Spring IOC 中获取 Spring 管理的 user 对象
User user = applicationContext.getBean("user", User.class);
**/
return "success";
}
}

Spring IOC 容器和 SpringMVC IOC 容器的关系

Spring IOC 容器是父容器,SpringMVC IOC 容器是子容器。子容器能够调用访问父容器中的 bean,而父容器不能调用访问子容器的 bean。

  1. 也就是说 Controller 中能够访问 Service bean 和 Dao bean。但是 Service 或 Dao 不能调用 Controller bean。
  2. Service 和 Dao 都受 Spring 管理,他们之间可以相互调用。

代码地址